﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using STEM123.StemService;

namespace STEM123
{
    /// <summary>
    /// Provides content creation facility using image and audio.
    /// Audio recording / saving source in this class is based on documentation and code samples at following locations:
    /// http://msdn.microsoft.com/en-us/library/gg442302(v=vs.92).aspx
    /// http://msdn.microsoft.com/en-us/library/dd757929%28VS.85%29.aspx
    /// http://damianblog.com/2011/02/07/storing-wp7-recorded-audio-as-wav-format-streams/ 
    /// </summary>
    public partial class CreateContentAudio : PhoneApplicationPage
    {
        #region Private members

        /// <summary>
        /// States of audio content management.
        /// </summary>
        private enum AudioContentStates { Error, Idle, Recording, Playing };

        /// <summary>
        /// Index of currently edited slide.
        /// </summary>
        private uint slideIndex;

        /// <summary>
        /// Uri of temporary image and audio file.
        /// </summary>
        private Uri tempImageUri, tempAudioUri;

        /// <summary>
        /// Explain view model instance.
        /// </summary>
        private ExplainViewModel viewModel;

        /// <summary>
        /// Microphone device.
        /// </summary>
        private Microphone microphone;

        /// <summary>
        /// Stream to store audio data.
        /// </summary>
        private MemoryStream stream;

        /// <summary>
        /// Instance used to play / keep track of audio being played.
        /// </summary>
        private SoundEffectInstance soundInstance;

        /// <summary>
        /// Current state of audio content creation.
        /// </summary>
        private AudioContentStates currentState;

        /// <summary>
        /// Timer used to perform certain operations periodically.
        /// </summary>
        private DispatcherTimer dt;
        
        /// <summary>
        /// Timer used to keep track of audio recording duration.
        /// </summary>
        private Timer audioTimer;

        /// <summary>
        /// Total duration of audio recording.
        /// </summary>
        private uint audioDuration;

        /// <summary>
        /// Saves content and navigates back to slide creation.
        /// </summary>
        /// <param name="sender">Button reference.</param>
        /// <param name="e">Event specific arguments.</param>
        private void SaveContent(object sender, EventArgs e)
        {
            // get reference of current slide
            LocalSlide localSlide = viewModel.Slides[slideIndex] as LocalSlide;
            
            // set default values for audio slide
            localSlide.MediaType = MediaTypes.AudioOnly;
            localSlide.Answer = null;

            // set media source and duration, if any
            if (tempAudioUri != null)
            {
                localSlide.LocalMediaSource = viewModel.SetAudio(slideIndex, true);
                localSlide.MediaDuration = audioDuration;
            }
            else
            {
                // if previously, slide contains non-audio content, remove it
                if (localSlide.LocalMediaSource != null)
                {
                    string fileName = ExplainViewModel.GetFileName(localSlide.LocalMediaSource);
                    if (!fileName.ToUpperInvariant().EndsWith(ContentTypes.WAV.ToString()))
                    {
                        localSlide.LocalMediaSource = null;
                        localSlide.MediaDuration = uint.MinValue;
                    }
                }
            }

            // set image source if only it has changed or if an image is already there
            if (tempImageUri != null)
            {
                localSlide.MediaType = MediaTypes.AudioWithImage;
                localSlide.LocalImageSource = viewModel.SetImage(slideIndex, true);
            }
            else if (localSlide.LocalImageSource != null)
            {
                localSlide.MediaType = MediaTypes.AudioWithImage;
            }

            // navigate
            NavigationService.Navigate(new Uri("/CreateSlides.xaml", UriKind.Relative));
        }

        /// <summary>
        /// Cancels saving of content and navigates back to slide creation / editing.
        /// </summary>
        /// <param name="sender">Button reference.</param>
        /// <param name="e">Event specific arguments.</param>
        private void CancelContent(object sender, EventArgs e)
        {
            // cancel saving state
            viewModel.SetAudio(slideIndex, false);
            viewModel.SetImage(slideIndex, false);

            // navigate
            NavigationService.Navigate(new Uri("/CreateSlides.xaml", UriKind.Relative));
        }

        /// <summary>
        /// Stops playing audio and updates state and ui elements.
        /// </summary>
        private void stopPlaying()
        {
            // stop playing
            soundInstance.Stop();
            soundInstance.Dispose();

            // update state
            currentState = AudioContentStates.Idle;

            // update ui elements
            btnRecordStop.IsEnabled = true;

            ImageBrush playPauseBrush = new ImageBrush();
            playPauseBrush.ImageSource = new BitmapImage(new Uri("Images/play.png", UriKind.Relative));
            btnPlayPause.Background = playPauseBrush;
        }

        /// <summary>
        /// Writes wav file header to a stream. Refer to http://msdn.microsoft.com/en-us/library/dd757929%28VS.85%29.aspx
        /// for background information. 
        /// Source is re-used from http://damianblog.com/2011/02/07/storing-wp7-recorded-audio-as-wav-format-streams/
        /// with minor modifications.
        /// </summary>
        /// <param name="sampleRate">Sampling rate at which audio is captured.</param>
        private void writeWavHeader(int sampleRate)
        {
            const int bitsPerSample = 16;
            const int bytesPerSample = bitsPerSample / 8;
            var encoding = System.Text.Encoding.UTF8;

            // ChunkID Contains the letters "RIFF" in ASCII form (0x52494646 big-endian form).
            stream.Write(encoding.GetBytes("RIFF"), 0, 4);

            // NOTE this will be filled in later
            stream.Write(BitConverter.GetBytes(0), 0, 4);

            // Format Contains the letters "WAVE"(0x57415645 big-endian form).
            stream.Write(encoding.GetBytes("WAVE"), 0, 4);

            // Subchunk1ID Contains the letters "fmt " (0x666d7420 big-endian form).
            stream.Write(encoding.GetBytes("fmt "), 0, 4);

            // Subchunk1Size 16 for PCM.  This is the size of therest of the Subchunk which follows this number.
            stream.Write(BitConverter.GetBytes(16), 0, 4);

            // AudioFormat PCM = 1 (i.e. Linear quantization) Values other than 1 indicate some form of compression.
            stream.Write(BitConverter.GetBytes((short)1), 0, 2);

            // NumChannels Mono = 1, Stereo = 2, etc.
            stream.Write(BitConverter.GetBytes((short)1), 0, 2);

            // SampleRate 8000, 44100, etc.
            stream.Write(BitConverter.GetBytes(sampleRate), 0, 4);

            // ByteRate =  SampleRate * NumChannels * BitsPerSample/8
            stream.Write(BitConverter.GetBytes(sampleRate * bytesPerSample), 0, 4);

            // BlockAlign NumChannels * BitsPerSample/8 The number of bytes for one sample including all channels.
            stream.Write(BitConverter.GetBytes((short)(bytesPerSample)), 0, 2);

            // BitsPerSample    8 bits = 8, 16 bits = 16, etc.
            stream.Write(BitConverter.GetBytes((short)(bitsPerSample)), 0, 2);

            // Subchunk2ID Contains the letters "data" (0x64617461 big-endian form).
            stream.Write(encoding.GetBytes("data"), 0, 4);

            // NOTE to be filled in later
            stream.Write(BitConverter.GetBytes(0), 0, 4);
        }

        /// <summary>
        /// Updates wave header with information about length of audio content.
        /// Source is re-used from http://damianblog.com/2011/02/07/storing-wp7-recorded-audio-as-wav-format-streams/
        /// with minor modifications.
        /// </summary>
        private void updateWavHeader()
        {
            if (!stream.CanSeek) throw new Exception("Can't seek stream to update wav header");

            var oldPos = stream.Position;

            // ChunkSize  36 + SubChunk2Size
            stream.Seek(4, SeekOrigin.Begin);
            stream.Write(BitConverter.GetBytes((int)stream.Length - 8), 0, 4);

            // Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8 This is the number of bytes in the data.
            stream.Seek(40, SeekOrigin.Begin);
            stream.Write(BitConverter.GetBytes((int)stream.Length - 44), 0, 4);

            stream.Seek(oldPos, SeekOrigin.Begin);
        }

        /// <summary>
        /// Stops recording and updates state.
        /// </summary>
        private void stopRecordingUpdateState()
        {
            // stop video recording timer
            audioTimer.Change(Timeout.Infinite, Timeout.Infinite);
            audioTimer.Dispose();
            audioTimer = null;

            // stop recording
            microphone.Stop();

            // read last set of bytes
            byte[] buffer = new byte[microphone.GetSampleSizeInBytes(microphone.BufferDuration)];
            microphone.GetData(buffer);
            stream.Write(buffer, 0, buffer.Length);

            // update header with length of data
            updateWavHeader();

            // store audio locally
            tempAudioUri = viewModel.WriteAudio(slideIndex, stream.ToArray(), true);

            // update state
            currentState = AudioContentStates.Idle;

            // update ui elements
            btnPlayPause.IsEnabled = true;

            ImageBrush recordStopbrush = new ImageBrush();
            recordStopbrush.ImageSource = new BitmapImage(new Uri("Images/record.png", UriKind.Relative));
            btnRecordStop.Background = recordStopbrush;

            txtblockSlide.Text = "Slide " + (slideIndex + 1);
        }

        /// <summary>
        /// Occurs whenever microphone buffer is ready to be read.
        /// </summary>
        /// <param name="sender">Reference to microphone.</param>
        /// <param name="e">Event specific arguments.</param>
        private void microphone_BufferReady(object sender, EventArgs e)
        {
            // read incoming bytes
            byte[] buffer = new byte[microphone.GetSampleSizeInBytes(microphone.BufferDuration)];
            microphone.GetData(buffer);

            // write them to stream
            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Occurs when user taps on image.
        /// </summary>
        /// <param name="sender">Image reference.</param>
        /// <param name="e">Event specific arguments.</param>
        private void imageAnswer_Tap(object sender, System.Windows.Input.GestureEventArgs e)
        {
            try
            {
                PhotoChooserTask getPhoto = new PhotoChooserTask();

                getPhoto.ShowCamera = true;

                // completed event is not fired on UI thread, hence call asynchronously
                getPhoto.Completed += (photoTask, photoEvent) =>
                    Dispatcher.BeginInvoke(() =>
                    {
                        BitmapImage bitmapAnswer = new BitmapImage();

                        if (photoEvent.ChosenPhoto != null)
                        {
                            // display image
                            bitmapAnswer.SetSource(photoEvent.ChosenPhoto);
                            imageAnswer.Source = bitmapAnswer;
                            txtblockImageGuidance.Visibility = Visibility.Collapsed;

                            // save temporarily
                            tempImageUri = viewModel.WriteImage(slideIndex, photoEvent.ChosenPhoto, true);
                        }
                    });

                getPhoto.Show();
            }
            catch (InvalidOperationException photoChooseException)
            {
                System.Diagnostics.Debug.WriteLine(photoChooseException.ToString());
            }
        }

        /// <summary>
        /// Occurs when user clicks on record / stop button. Acts as toggle.
        /// </summary>
        /// <param name="sender">Button reference.</param>
        /// <param name="e">Event specific arguments.</param>
        private void btnRecordStop_Click(object sender, RoutedEventArgs e)
        {
            if (currentState == AudioContentStates.Idle)
            {
                // recreate stream for new recording
                stream.Dispose();
                stream = new MemoryStream();

                // write header information to stream first
                writeWavHeader(microphone.SampleRate);

                // initialize microphone and begin recording
                microphone.BufferDuration = TimeSpan.FromMilliseconds(1000);
                microphone.Start();

                // update current state
                currentState = AudioContentStates.Recording;

                // update ui elements
                btnPlayPause.IsEnabled = false;

                ImageBrush recordStopbrush = new ImageBrush();
                recordStopbrush.ImageSource = new BitmapImage(new Uri("Images/stop.png", UriKind.Relative));
                btnRecordStop.Background = recordStopbrush;

                audioDuration = 0;
                audioTimer = new Timer(new TimerCallback((state) =>
                {
                    audioDuration++;

                    if (audioDuration > 30)
                    {
                        Dispatcher.BeginInvoke(() => stopRecordingUpdateState());
                    }
                    else
                    {
                        Dispatcher.BeginInvoke(() => txtblockSlide.Text = (30 - audioDuration) + " seconds left.");
                    }
                }
                ), null, 0, 1000);
            }
            else if (currentState == AudioContentStates.Recording)
            {
                stopRecordingUpdateState();
            }
        }

        /// <summary>
        /// Occurs when user clicks on play / pause button. Acts as toggle.
        /// </summary>
        /// <param name="sender">Button reference.</param>
        /// <param name="e">Event specific arguments.</param>
        private void btnPlayPause_Click(object sender, RoutedEventArgs e)
        {
            if (currentState == AudioContentStates.Idle)
            {
                // play audio in a separate thread
                Thread playthread = new Thread(() =>
                {
                    SoundEffect sound = new SoundEffect(stream.ToArray(), microphone.SampleRate, AudioChannels.Mono);
                    soundInstance = sound.CreateInstance();
                    soundInstance.Play();
                });

                playthread.Start();

                // update state
                currentState = AudioContentStates.Playing;
                
                // update ui elements
                btnRecordStop.IsEnabled = false;

                ImageBrush playPauseBrush = new ImageBrush();
                playPauseBrush.ImageSource = new BitmapImage(new Uri("Images/stop.png", UriKind.Relative));
                btnPlayPause.Background = playPauseBrush;
            }
            else if (currentState == AudioContentStates.Playing)
            {
                stopPlaying();
            }
        }

        #endregion

        #region Protected members

        /// <summary>
        /// Occurs when page is navigated to, from some other page.
        /// </summary>
        /// <param name="e">Navigation event arguments.</param>
        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            // read input params
            IDictionary<string, string> parameters = NavigationContext.QueryString;
            slideIndex = uint.Parse(parameters["index"]);

            // initialize UI elements by reading state
            txtblockTerm.Text = ExplainViewModel.Instance.Term;
            txtblockQuestion.Text = ExplainViewModel.Instance.Slides[slideIndex].Question;
            txtblockSlide.Text = "Slide " + (slideIndex + 1);

            // get reference of currently edited slide
            LocalSlide localSlide = viewModel.Slides[slideIndex] as LocalSlide;

            if (localSlide.LocalImageSource != null)
            {
                imageAnswer.Source = viewModel.ReadImage(localSlide.LocalImageSource);
                txtblockImageGuidance.Visibility = System.Windows.Visibility.Collapsed;
            }

            if (localSlide.LocalMediaSource != null)
            {
                stream = viewModel.ReadAudio(localSlide.LocalMediaSource);
            }
            else
            {
                btnPlayPause.IsEnabled = false;
            }

            // simulate game loop using timer to 
            // update status of media components, i.e. audio
            dt.Start();

            // hook to microphone's buffer ready event to read incoming audio data
            microphone.BufferReady += new EventHandler<EventArgs>(microphone_BufferReady);
        }

        /// <summary>
        /// Occurs when page is navigated from, to other page.
        /// </summary>
        /// <param name="e">Event specific arguments.</param>
        protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);

            // stop timer 
            dt.Stop();

            if (audioTimer != null)
            {
                audioTimer.Change(Timeout.Infinite, Timeout.Infinite);
                audioTimer.Dispose();
                audioTimer = null;
            }

            // stop recording (if any) and unhook from buffer ready event of microphone
            microphone.Stop();
            microphone.BufferReady -= new EventHandler<EventArgs>(microphone_BufferReady);
        }

        #endregion

        #region Public members

        /// <summary>
        /// Initializes a new instance of CreateContentAudio.
        /// </summary>
        public CreateContentAudio()
        {
            InitializeComponent();

            // initialize member variables
            viewModel = ExplainViewModel.Instance;
            tempImageUri = null;
            currentState = AudioContentStates.Idle;
            tempAudioUri = null;
            microphone = Microphone.Default;
            stream = new MemoryStream();

            // initialize timer to perform media updates periodically
            dt = new DispatcherTimer();
            dt.Interval = TimeSpan.FromMilliseconds(50);
            dt.Tick += delegate
            {
                try
                {
                    FrameworkDispatcher.Update();

                    if (currentState == AudioContentStates.Playing)
                    {
                        if (soundInstance != null && soundInstance.State == SoundState.Stopped)
                        {
                            Dispatcher.BeginInvoke(() => stopPlaying());
                        }
                    }
                }
                catch
                {
                }
            };
        }

        #endregion
    }
}